/*
 * OREKIT-X
 * Copyright 2002-2008 CS Communication & Systemes
 * 
 * Parts of this software package have been licensed to CS
 * Communication & Systemes (CS) under one or more contributor license
 * agreements.  See the NOTICE file distributed with this work for
 * additional information.
 *  
 * This is an experimental copy of OREKIT from www.orekit.org.
 * Please use the original OREKIT from orekit.org for normal work
 * unrelated to this research project.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You
 * may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package ore.bodies;

import ore.CelestialBody;
import ore.Frame;
import ore.PositionVelocity;
import ore.PositionVelocityChebyshev;
import ore.Transform;
import ore.errors.OrekitException;
import ore.frames.FramesFactory;
import ore.time.AbsoluteDate;
import ore.time.ChronologicalComparator;
import ore.time.TimeStamped;

import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;

/** 
 * The underlying body-centered frames are either direct children of
 * {@link Frame#getEME2000() EME2000} (for {@link Moon} and {@link
 * #getEarthMoonBarycenter() Earth-Moon barycenter}) or children from
 * other body-centered frames. For example, the path from EME2000 to
 * Jupiter-centered frame is: EME2000, Earth-Moon barycenter centered,
 * solar system barycenter centered, Jupiter-centered.
 * 
 * The defining transforms of these frames are combinations of simple
 * linear transforms without any rotation. The frame axes are
 * therefore always parallel to {@link Frame#getEME2000() EME2000}
 * frame axes.
 * 
 * The position of the bodies provided by this class are interpolated
 * using the JPL DE 405/DE 406 ephemerides.
 * 
 * @author Luc Maisonobe
 */
public class SolarSystemBody
    extends AbstractCelestialBody
{


    private static CelestialBody solarSystemBarycenter;

    private static CelestialBody earthMoonBarycenter;


    private final SortedSet<TimeStamped> ephemeris;

    private final JPLEphemerides.EphemerisType type;

    private final Frame definingFrame;

    private PositionVelocityChebyshev model;

    private double earthMoonMassRatio;


    /** 
     * @param gm attraction coefficient (in m<sup>3</sup>/s<sup>2</sup>)
     * @param definingFrame frame in which ephemeris are defined
     * @param type DE 405 ephemeris type
     * @param frameName name to use for the body-centered frame
     */
    protected SolarSystemBody( double gm, Frame definingFrame,
                               JPLEphemerides.EphemerisType type,
                               String frameName)
    {
        super(gm, frameName, definingFrame);
        this.ephemeris     = new TreeSet<TimeStamped>(new ChronologicalComparator());
        this.model         = null;
        this.type          = type;
        this.definingFrame = definingFrame;
    }

    /** 
     * @param definingFrame frame in which ephemeris are defined
     * @param type DE 405 ephemeris type
     * @param frameName name to use for the body-centered frame
     * 
     * @exception OrekitException if the header constants cannot be read
     */
    protected SolarSystemBody(Frame definingFrame,
                              JPLEphemerides.EphemerisType type,
                              String frameName)
        throws OrekitException
    {
        this(JPLConstants.GetGravitationalCoefficient(type), definingFrame, type, frameName);
    }



    /** {@inheritDoc}
     */
    public PositionVelocity getPositionVelocity(final AbsoluteDate date, final Frame frame)
        throws OrekitException
    {
        /*
         * Get position/velocity in parent frame
         */
        this.setPVModel(date);

        PositionVelocity pv = model.getPositionVelocity(date);
        /*
         * Convert to required frame
         */
        if (frame == definingFrame)
            return pv;
        else {
            Transform transform = definingFrame.getTransformTo(frame, date);
            return transform.transformPositionVelocity(pv);
        }
    }

    /** Get the Earth-Moon mass ratio.
     * @return Earth-Moon mass ratio
     */
    protected double getEarthMoonMassRatio() {
        return earthMoonMassRatio;
    }

    /** Set the position-velocity model covering a specified date.
     * @param date target date
     * @exception OrekitException if current date is not covered by
     * available ephemerides
     */
    private void setPVModel(final AbsoluteDate date)
        throws OrekitException {

        // first quick check: is the current model valid for specified date ?
        if (model != null) {

            if (model.inRange(date)) {
                return;
            }

            // try searching only within the already loaded ephemeris part
            final AbsoluteDate before = new AbsoluteDate(date, -model.getValidityDuration());
            for (final Iterator<TimeStamped> iterator = ephemeris.tailSet(before).iterator();
                 iterator.hasNext();) {
                model = (PositionVelocityChebyshev) iterator.next();
                if (model.inRange(date)) {
                    return;
                }
            }

        }
        /*
         * Existing ephemeris (if any) is too far from current date
         * load a new part of ephemeris, centered around specified date
         */
        final JPLEphemerides loader = new ore.io.JPLEphemeridesLoader(type, date);
        this.ephemeris.addAll(loader);
        this.earthMoonMassRatio = JPLConstants.GetEarthMoonMassRatio();

        final AbsoluteDate before = new AbsoluteDate(date, -loader.getMaxChunksDuration());
        /*
         * Validate
         */
        for (final Iterator<TimeStamped> iterator = this.ephemeris.tailSet(before).iterator();
             iterator.hasNext();)
        {
            model = (PositionVelocityChebyshev) iterator.next();
            if (model.inRange(date)) {
                return;
            }
        }
        /*
         * Don't have data for this date
         */
        throw new OrekitException("out of range date for {0} ephemerides: {1}",
                                  type, date);
    }

    /** Get the solar system barycenter aggregated body.
     * <p>The axes of the body-centered frame linked to this instance
     * are parallel to the {@link Frame#getEME2000() EME2000} frame.</p>
     * @return solar system barycenter aggregated body
     * @exception OrekitException if the JPL ephemerides cannot be read
     */
    public static CelestialBody getSolarSystemBarycenter() throws OrekitException {
        synchronized (CelestialBody.class) {
            if (solarSystemBarycenter == null) {

                final double gmSum =
                    JPLConstants.GetGravitationalCoefficient(JPLEphemerides.EphemerisType.SUN)        +
                    JPLConstants.GetGravitationalCoefficient(JPLEphemerides.EphemerisType.MERCURY)    +
                    JPLConstants.GetGravitationalCoefficient(JPLEphemerides.EphemerisType.VENUS)      +
                    JPLConstants.GetGravitationalCoefficient(JPLEphemerides.EphemerisType.EARTH_MOON) +
                    JPLConstants.GetGravitationalCoefficient(JPLEphemerides.EphemerisType.MARS)       +
                    JPLConstants.GetGravitationalCoefficient(JPLEphemerides.EphemerisType.JUPITER)    +
                    JPLConstants.GetGravitationalCoefficient(JPLEphemerides.EphemerisType.SATURN)     +
                    JPLConstants.GetGravitationalCoefficient(JPLEphemerides.EphemerisType.URANUS)     +
                    JPLConstants.GetGravitationalCoefficient(JPLEphemerides.EphemerisType.NEPTUNE)    +
                    JPLConstants.GetGravitationalCoefficient(JPLEphemerides.EphemerisType.PLUTO);
                solarSystemBarycenter = new SolarSystemBody(gmSum,
                                                            SolarSystemBody.getEarthMoonBarycenter().getFrame(),
                                                            JPLEphemerides.EphemerisType.EARTH_MOON,
                                                            "solar system centered EME2000") {

                    /** Serializable UID. */
                    private static final long serialVersionUID = 7350102501303428347L;

                    /** {@inheritDoc} */
                    public PositionVelocity getPositionVelocity(final AbsoluteDate date, final Frame frame)
                        throws OrekitException {
                        // we define solar system barycenter with respect to Earth-Moon barycenter
                        // so we need to revert the vectors provided by the JPL DE 405 ephemerides
                        final PositionVelocity emPV = super.getPositionVelocity(date, frame);
                        return new PositionVelocity(emPV.getPosition().negate(), emPV.getVelocity().negate());
                    }

                };
            }

            return solarSystemBarycenter;

        }
    }


    /** Get the Earth-Moon barycenter singleton bodies pair.
     * <p>The axes of the body-centered frame linked to this instance
     * are parallel to the {@link Frame#getEME2000() EME2000} frame.</p>
     * @return Earth-Moon barycenter bodies pair
     * @exception OrekitException if the JPL ephemerides cannot be read
     */
    public static CelestialBody getEarthMoonBarycenter() throws OrekitException {
        synchronized (CelestialBody.class) {
            if (earthMoonBarycenter == null) {
                final double moonGM  =
                    JPLConstants.GetGravitationalCoefficient(JPLEphemerides.EphemerisType.MOON);
                final double earthGM =
                    JPLConstants.GetEarthMoonMassRatio() * moonGM;
                earthMoonBarycenter = new SolarSystemBody(earthGM + moonGM, FramesFactory.getEME2000(),
                                                          JPLEphemerides.EphemerisType.MOON,
                                                          "Earth-Moon centered EME2000") {

                    /** Serializable UID. */
                    private static final long serialVersionUID = -6860799524750318529L;

                    /** {@inheritDoc} */
                    public PositionVelocity getPositionVelocity(final AbsoluteDate date, final Frame frame)
                        throws OrekitException {
                        // we define Earth-Moon barycenter with respect to Earth center so we need
                        // to apply a scale factor to the Moon vectors provided by the JPL DE 405 ephemerides
                        final PositionVelocity moonPV = super.getPositionVelocity(date, frame);

                        // since we have computed moonPV, we know the ephemeris has been read
                        // so now we know the Earth-Moon ratio is available
                        final double scale = 1.0 / (1.0 + getEarthMoonMassRatio());

                        return new PositionVelocity(scale, moonPV);
                    }

                };
            }

            return earthMoonBarycenter;

        }

    }

}
